/**
 * $RCSfile: GatewayManager.java,v $
 * $Revision: 1.11 $
 * $Date: 2001/09/22 17:43:18 $
 *
 * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved.
 *
 * This software is the proprietary information of CoolServlets, Inc.
 * Use is subject to license terms.
 */

package com.jivesoftware.forum.gateway;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.InvalidPropertiesFormatException;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import com.jivesoftware.forum.Forum;
import com.jivesoftware.forum.ForumFactory;
import com.jivesoftware.forum.ForumMessage;
import com.jivesoftware.forum.ForumThread;
import com.jivesoftware.forum.JiveGlobals;
import com.jivesoftware.forum.util.TaskEngine;
import com.jivesoftware.util.BeanUtils;
import com.jivesoftware.util.StringUtils;
import com.jivesoftware.util.TimerTask;

/**
 * Manages gateways for a forum, which synchronize the forum with external data
 * sources such as an NNTP newsgroup or email account.
 * 
 * Notes: The default GatewayManager implementation stores configuration
 * properties in a file called <tt>gateway_config.xml</tt> under the </tt>jiveHome</tt>
 * directory. The default implementation also only knows how to instantiate
 * Gateway implementations that have either a default constructor or one that
 * accepts a ForumFactory and Forum as arguments.
 * 
 * @see Gateway
 */
public class GatewayManager {

	/**
	 * The list of the class names of the gateways that are available for
	 * installation by default. These values are automatically added into the
	 * <tt>jive_gateways.xml</tt> file when it is first created. Further
	 * classes can be defined by editing the
	 * <tt>jiveGateways.gatewayClasses</tt> property.
	 */
	public static final String[] DEFAULT_GATEWAY_CLASSES = new String[] {
			"com.jivesoftware.forum.gateway.EmailGateway",
			"com.jivesoftware.forum.gateway.NewsgroupGateway" };

	private static Properties properties = null;

	private Gateway[] gateways;

	/**
	 * Context for this manager instance in the XML property file.
	 */
	private String context = null;
	/**
	 * The scheduled task for gateway imports.
	 */
	private TimerTask timerTask = null;
	private long lastImport;

	private boolean importEnabled = true;
	private boolean exportEnabled = true;
	private String exportFooter = null;
	private int importInterval = 15;

	/**
	 * Creates a new DbGatewayManager. It will load existing values from the
	 * persistent store.
	 */
	public GatewayManager(ForumFactory factory, Forum forum) {
		loadProperties();
		String name = "forum" + forum.getID();
		context = name + ".";

		// load up gateways for this manager.
		String gCount = properties.getProperty(context + "gatewayCount");
		if (gCount == null) {
			gCount = "0";
		}
		int gatewayCount = 0;
		try {
			gatewayCount = Integer.parseInt(gCount);
		} catch (NumberFormatException nfe) {
		}

		// See if the time that the last import was performed is stored.
		String importTimestamp = properties.getProperty(context + "lastImport");
		if (importTimestamp != null) {
			try {
				lastImport = Long.parseLong(importTimestamp);
			} catch (NumberFormatException nfe) {
				lastImport = System.currentTimeMillis();
			}
		}
		// Not stored, so create new timestamp.
		else {
			lastImport = System.currentTimeMillis();
		}

		// Load up all gateways
		gateways = new Gateway[gatewayCount];

		for (int i = 0; i < gatewayCount; i++) {
			try {
				String gatewayContext = context + "gateway" + i + ".";
				String className = properties.getProperty(gatewayContext
						+ "className");
				Class gClass = Class.forName(className);
				// First, attempt to instantiate the Gateway with a ForumFactory
				// and Forum as paramaters. If that doesn't work, we'll try
				// the default constructor.
				try {
					Class[] params = new Class[] { ForumFactory.class,
							Forum.class };
					Constructor constructor = gClass.getConstructor(params);
					// Intantiate the gateway object. We assume that
					gateways[i] = (Gateway) constructor
							.newInstance(new Object[] { factory, forum });
				} catch (NoSuchMethodException e) {
					gateways[i] = (Gateway) gClass.newInstance();
				}
				// Load gateway properties.
				Enumeration<?> allPropNames = properties.propertyNames();
				String propNamePrefix = gatewayContext + "properties";
				Map<String, String> gatewayProps = new HashMap();
				String propName = null;
				while (allPropNames.hasMoreElements()) {
					// Get the bean property name, which is everything after
					// the last '.' in the xml property name.
					propName = allPropNames.nextElement().toString();
					if (propName.startsWith(propNamePrefix)) {

						gatewayProps.put(propName.substring(propNamePrefix
								.length()), properties.getProperty(propName));
					}
				}
				// Set properties on the bean
				BeanUtils.setProperties(gateways[i], gatewayProps);
			} catch (Exception e) {
				System.err.println("Error loading gateway " + i
						+ " for context " + context);
				e.printStackTrace();
			}
		}

		// Now load general properties
		try {
			String propValue = properties
					.getProperty(context + "importEnabled");
			if (propValue != null) {
				this.importEnabled = Boolean.valueOf(propValue).booleanValue();
			}
			propValue = properties.getProperty(context + "exportEnabled");
			if (propValue != null) {
				this.exportEnabled = Boolean.valueOf(propValue).booleanValue();
			}
			propValue = properties.getProperty(context + "importInterval");
			if (propValue != null) {
				this.setImportInterval(Integer.parseInt(propValue));
			}
			exportFooter = properties.getProperty(context + "exportFooter");
		} catch (NumberFormatException nfe) { /* ignore */
		}

		this.startImportTask();
	}

	/**
	 * Returns true if gateway importing is turned on. When importing is on, the
	 * importData method of each installed gateway will be invoked at the
	 * specified import interval. By default, importing is enabled.
	 * 
	 * @return true if gateway imports are enabled.
	 */
	public boolean isImportEnabled() {
		return importEnabled;
	}

	/**
	 * Toggles gateway importing on or off. When importing is on, the importData
	 * method of each installed gateway will be invoked at the specified import
	 * interval. By default, importing is enabled.
	 * 
	 * @param importEnabled
	 *            true if gateway importing should be enabled.
	 */
	public void setImportEnabled(boolean importEnabled) {
		this.importEnabled = importEnabled;
		properties.setProperty(context + "importEnabled", "" + importEnabled);
	}

	/**
	 * Returns true if gateway exporting is turned on. When exporting is on, the
	 * exportData method of each installed gateway will be invoked on each new
	 * message that is created in the forum. By default, exporting is enabled.
	 * 
	 * @return true if gateway exports are enabled.
	 */
	public boolean isExportEnabled() {
		return exportEnabled;
	}

	/**
	 * Toggles gateway exporting on or off. When exporting is on, the exportData
	 * method of each installed gateway will be invoked on each new message that
	 * is created in the forum. By default, exporting is enabled.
	 * 
	 * @param exportEnabled
	 *            true if gateway exporting should be enabled.
	 */
	public void setExportEnabled(boolean exportEnabled) {
		this.exportEnabled = exportEnabled;
		properties.setProperty(context + "exportEnabled", "" + exportEnabled);
	}

	/**
	 * Returns the footer that will be appended to messages as they're exported.
	 * By default, this value is <tt>null</tt>, which means that no footer
	 * will be used.
	 * <p>
	 * 
	 * A number of tokens can be inserted into the filter. Each token will be
	 * dynamically replaced as the message is sent out with the real value.
	 * Valid tokens are:
	 * <ul>
	 * {threadID}, {threadName}, {forumID}, {forumName}, {messageID}
	 * </ul>
	 * 
	 * @return the footer that will be appended to messages being exported.
	 */
	public String getExportFooter() {
		return exportFooter;
	}

	/**
	 * Sets the footer that will be appended to messages as they're exported. By
	 * default, this value is <tt>null</tt>, which means that no footer will
	 * be used.
	 * <p>
	 * 
	 * A number of tokens can be inserted into the filter. Each token will be
	 * dynamically replaced as the message is sent out with the real value.
	 * Valid tokens are:
	 * <ul>
	 * {threadID}, {threadName}, {forumID}, {forumName}, {messageID}
	 * </ul>
	 * 
	 * @param exportFooter
	 *            the footer that will be appended to messages being exported.
	 */
	public void setExportFooter(String exportFooter) {
		this.exportFooter = exportFooter;
		// If we are setting the value to null, we should delete the property.
		if (exportFooter == null) {
			properties.remove(context + "exportFooter");
		}
		// Otherwise, set the property with the new value.
		else {
			properties.setProperty(context + "exportFooter", exportFooter);
		}
	}

	/**
	 * Returns the number of minutes that the manager waits between each gateway
	 * import. Default is 15 minutes.
	 * 
	 * @return the number of minutes between automatic index updates.
	 */
	public int getImportInterval() {
		return importInterval;
	}

	/**
	 * Sets the number of minutes that the manager waits between each gateway
	 * import. Default is 15 minutes.
	 */
	public void setImportInterval(int importInterval) {
		this.importInterval = importInterval;
		properties.setProperty(context + "importInterval", "" + importInterval);
	}

	/**
	 * Returns the Gateway at the specified index.
	 * 
	 * @return the filter at the specified index.
	 */
	public Gateway getGateway(int index) {
		if (index < 0 || index > gateways.length - 1) {
			throw new IllegalArgumentException("Index " + index
					+ " is not valid.");
		}
		return gateways[index];
	}

	/**
	 * Returns the count of currently active gateways for the forum.
	 * 
	 * @return a count of the currently active filters.
	 */
	public int getGatewayCount() {
		return gateways.length;
	}

	/**
	 * Adds a new Gateway to the end of the gateway list.
	 * 
	 * @param gateway
	 *            Gateway to add to the gateway list.
	 */
	public void addGateway(Gateway gateway) {
		addGateway(gateway, gateways.length);
	}

	/**
	 * Inserts a new Gateway at specified index in the gateway list.
	 * 
	 * @param filter
	 *            Gateway to add to the gateeway list.
	 * @param index
	 *            position in gateway list to insert new gateway.
	 */
	public void addGateway(Gateway gateway, int index) {
		ArrayList newGateways = new ArrayList(gateways.length + 1);
		for (int i = 0; i < gateways.length; i++) {
			newGateways.add(gateways[i]);
		}
		newGateways.add(index, gateway);
		Gateway[] newArray = new Gateway[newGateways.size()];
		for (int i = 0; i < newArray.length; i++) {
			newArray[i] = (Gateway) newGateways.get(i);
		}
		// Finally, overwrite gateways with the new array
		gateways = newArray;
		saveGateways();
	}

	/**
	 * Removes a Gateway at the specified index in the gateway list.
	 * 
	 * @param index
	 *            position in gateway list to remove gateway from.
	 */
	public void removeGateway(int index) {
		ArrayList newGateways = new ArrayList(gateways.length);
		for (int i = 0; i < gateways.length; i++) {
			newGateways.add(gateways[i]);
		}
		newGateways.remove(index);
		Gateway[] newArray = new Gateway[newGateways.size()];
		for (int i = 0; i < newArray.length; i++) {
			newArray[i] = (Gateway) newGateways.get(i);
		}
		// Finally, overwrite gateways with the new array
		gateways = newArray;
		saveGateways();
	}

	/**
	 * Saves all Gateways to the persistent store. This method should be called
	 * after setting any properties on individual gateways that are being
	 * managed by this gateway manager.
	 */
	public void saveGateways() {
		saveGateways(true);
	}

	/**
	 * Saves all Gateways to the persistent store. This method should be called
	 * after setting any properties on individual gateways that are being
	 * managed by this gateway manager.
	 */
	public void saveGateways(boolean restartImports) {
		// Delete the entire context.
		deleteContext();

		// Write out basic props.
		properties.setProperty(context + "importEnabled", "" + importEnabled);
		properties.setProperty(context + "exportEnabled", "" + exportEnabled);
		properties.setProperty(context + "importInterval", "" + importInterval);
		if (exportFooter != null) {
			properties.setProperty(context + "exportFooter", exportFooter);
		}

		// Write out all data. First, the count.
		if (gateways.length > 0) {
			properties.setProperty(context + "gatewayCount", Integer
					.toString(gateways.length));
		}
		// Next, the lastImport date
		properties.setProperty(context + "lastImport", String
				.valueOf(lastImport));
		// Each gateway...
		for (int i = 0; i < gateways.length; i++) {
			String gatewayContext = context + "gateway" + i + ".";
			// Write out class name.
			properties.setProperty(gatewayContext + "className", gateways[i]
					.getClass().getName());
			// Write out all properties.
			Map gatewayProps = BeanUtils.getProperties(gateways[i]);
			for (Iterator iter = gatewayProps.keySet().iterator(); iter
					.hasNext();) {
				String name = (String) iter.next();
				String value = (String) gatewayProps.get(name);
				properties.setProperty(gatewayContext + "properties." + name,
						value);
			}
		}

		// Since the gateway information likely changed, restart the import
		// task.
		if (restartImports) {
			this.startImportTask();
		}
	}

	/**
	 * Exports an individual message through all gateways. This method is only
	 * intended to be called by other intenral Jive classes.
	 */
	public void exportData(ForumMessage message) {
		if (gateways.length == 0) {
			return;
		}
		TaskEngine.addTask(new GatewayExportTask(message));
	}

	/**
	 * Deletes all properties for this context. This method is only intended to
	 * be called by other internal Jive classes.
	 */
	public void deleteContext() {
		properties.remove(context.substring(0, context.length() - 1));
	}

	/**
	 * Gets the export footer for a message, which is the footer with all tokens
	 * replaced with correct values.
	 */
	public String getTranslatedFooter(ForumMessage message) {
		if (exportFooter == null) {
			return "";
		}
		String result = exportFooter;
		ForumThread thread = message.getForumThread();
		Forum forum = thread.getForum();
		result = StringUtils.replace(result, "{threadID}", "" + thread.getID());
		result = StringUtils.replace(result, "{threadName}", thread.getName());
		result = StringUtils.replace(result, "{forumID}", "" + forum.getID());
		result = StringUtils.replace(result, "{forumName}", forum.getName());
		result = StringUtils.replace(result, "{messageID}", ""
				+ message.getID());
		return "\n" + result;
	}

	/**
	 * Start/restart the gateway import task
	 */
	private void startImportTask() {
		if (!importEnabled) {
			return;
		}

		// Restart timer with new schedule.
		if (timerTask != null) {
			timerTask.cancel();
		}
		timerTask = TaskEngine.scheduleTask(new GatewayImportTask(),
				importInterval * JiveGlobals.MINUTE, importInterval
						* JiveGlobals.MINUTE);
	}

	/**
	 * Loads a property manager for gateway persistence if it isn't already
	 * loaded. If an XML file for the gateways isn't already created, it will
	 * attempt to make a file with default value.
	 */
	private static void loadProperties() {
		if (properties == null) {
			String gatewayXML = JiveGlobals.getJiveHome() + File.separator
					+ "jive_gateways.xml";
			// Make sure the file actually exists. If it doesn't, a new file
			// will be created.
			File file = new File(gatewayXML);
			// Finally, create xml properties with the file.
			properties = new Properties();
			try {
				properties.loadFromXML(new FileInputStream(file));
			} catch (InvalidPropertiesFormatException e) {
				e.printStackTrace();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * A task that performs an import for all installed gateways.
	 */
	private class GatewayImportTask implements Runnable {

		public void run() {
			// If importing is turned off, return.
			if (!importEnabled) {
				return;
			}
			long now = System.currentTimeMillis();
			// Set the cutoff date for imports to be 24 hours before
			// the last import. This will give us a good buffer since
			// messages can get delayed in transport or other mail
			// servers can have their time set wrong. It's still
			// possible that we'll loose some messages even with a 24
			// hour buffer, but this occurrence should be quite rare.
			Date cutoffDate = new Date(lastImport - JiveGlobals.HOUR * 24);
			for (int i = 0; i < gateways.length; i++) {
				try {
					gateways[i].importData(cutoffDate);
				} catch (GatewayException ge) {
					ge.printStackTrace();
				}
			}
			// Done with the import so set the new import timestamp.
			lastImport = now;
			// Save the timestamp.
			properties.setProperty(context + "lastImport", String
					.valueOf(lastImport));
		}
	}

	/**
	 * A task that exports a message through all installed gateways.
	 */
	private class GatewayExportTask implements Runnable {

		ForumMessage message;

		GatewayExportTask(ForumMessage message) {
			this.message = message;
		}

		public void run() {
			// If exporting is turned off, return.
			if (!exportEnabled) {
				return;
			}

			for (int i = 0; i < gateways.length; i++) {
				try {
					gateways[i].exportData(message);
				} catch (GatewayException ge) {
					ge.printStackTrace();
				}
			}
		}
	}
}